import { v4 } from "uuid";
import { cloneDeep, kebabCase, camelCase, upperFirst } from "lodash-es";
import {
  ParsedFormItem,
  CodeGenContextBuilder,
  ObjectLiteralDesc,
  TagClosingStyle,
  TagNameStyle,
  VueComponetImport,
  VueViewSyntax,
  BaseImportDeclaration,
  ParsedComponent,
  VNode,
  ElementCodeGenConfig,
  ElementCodeGenContext,
  VueVModelSyntax,
  ArrayLiteralDesc,
} from "@/types";
import {
  getFirstVariable,
  getAllVariables,
  mergeImportDeclarations,
  parseImportDeclaration,
  generateImportDeclarationCode,
  generateObjectLiteral,
  generateFormObjectLiteral,
  transformStringAsTemplate,
  vnodeToString,
  stringifyExp,
  createVNode,
  isIdentifier,
} from "@/utils";
import { refineFormItems } from "@/features";

type Flags = {
  v2: boolean;
  v3: boolean;
  JSX: boolean;
  shoulImportBuiltInComps: boolean;
};

type BuildStep<R = string> = (options: {
  flags: Flags;
  items: ParsedFormItem[];
  config: ElementCodeGenConfig;
}) => R;

// <el-row></el-row>
function createLayoutVNode(options: {
  spans: number[];
  rowTag: string;
  colTag: string;
  fill: (index: number) => VNode;
}) {
  const { rowTag, colTag, spans, fill } = options;

  const cols = spans.map((span, index) =>
    createVNode({ tag: colTag, props: { span }, children: [fill(index)] })
  );

  return createVNode({
    tag: rowTag,
    children: cols,
  });
}

function createFormItemVNode(options: {
  item: ParsedFormItem;
  formItemTag: string;
  codeGenConfig: ElementCodeGenConfig;
  flags: Flags;
}) {
  const { item, formItemTag, codeGenConfig, flags } = options;
  const { label, field, component } = item;
  const { formAccessor, vModelSyntax, colonType } = codeGenConfig;
  const { v2, JSX, shoulImportBuiltInComps } = flags;

  const realInput = createVNode({
    tag: getFirstVariable(component.import)!,
    props: component.props,
  });

  if (!component.custom && v2 && !JSX && shoulImportBuiltInComps) {
    realInput.tag = `El${realInput.tag}`;
  }

  // bind value
  const propValue = isIdentifier(field)
    ? `${formAccessor}.${field}`
    : `${formAccessor}['${field}']`;
  realInput.props[
    VueVModelSyntax.VModel === vModelSyntax ? "v-model" : "value.sync"
  ] = propValue;

  return createVNode({
    tag: formItemTag,
    props: {
      label: `${label}${colonType}`,
      prop: field,
    },
    children: [realInput],
  });
}

const buildView: BuildStep = ({ items, flags, config }) => {
  const { v2, JSX } = flags;

  let rowTag: string, colTag: string, formItemTag: string;
  if (v2 && JSX) {
    // element-ui组件导出示例：import { Row } from "element-ui"
    rowTag = "Row";
    colTag = "Col";
    formItemTag = "FormItem";
  } else {
    rowTag = "ElRow";
    colTag = "ElCol";
    formItemTag = "ElFormItem";
  }

  const layoutNode = createLayoutVNode({
    spans: items.map((i) => i.span),
    rowTag,
    colTag,
    fill(index) {
      return createFormItemVNode({
        item: items[index],
        formItemTag,
        codeGenConfig: config,
        flags,
      });
    },
  });

  const { JSXPropFormatter, VueTemplatePropFormatter } =
    vnodeToString.propFormatters;

  return vnodeToString(layoutNode, {
    tag: {
      selfClosing: config.TagClosingStyle === TagClosingStyle.SingleTag,
      format:
        config.tagNameStyle == TagNameStyle.Camel
          ? (tag) => upperFirst(camelCase(tag))
          : kebabCase,
    },
    property: {
      format:
        config.viewSyntax === VueViewSyntax.Template
          ? VueTemplatePropFormatter
          : JSXPropFormatter,

      sort: ({ prop }) =>
        prop === "v-model" || prop === "value.sync" ? -1 : 0,
    },
  });
};

const buildRule: BuildStep = ({ items, config }) => {
  const ruleObjLiteralDec = items.reduce<ObjectLiteralDesc>(
    (ruleObjLiteralDec, item) => {
      const rules: ArrayLiteralDesc = [];
      const { required, label, field, regExps, validators } = item;

      if (required) {
        rules.push({
          required: stringifyExp(true),
          message: stringifyExp(
            transformStringAsTemplate(config.requirementPromptInfo, {
              label,
              field,
            })
          ),
        });
      }

      rules.push(
        ...regExps.map((regExp) => ({
          pattern: getFirstVariable(regExp.import)!,
          message: stringifyExp(
            transformStringAsTemplate(regExp.message, { label, field })
          ),
        }))
      );

      rules.push(
        ...validators.map((validator) => ({
          validator: getFirstVariable(validator.import)!,
        }))
      );

      if (rules.length) {
        ruleObjLiteralDec[field] = rules;
      }
      return ruleObjLiteralDec;
    },
    {}
  );
  return generateObjectLiteral(ruleObjLiteralDec);
};

const NECESSARY_IDS = {
  v2: [
    parseImportDeclaration('import { Form } from "element-ui"'),
    parseImportDeclaration('import { Row } from "element-ui"'),
    parseImportDeclaration('import { Col } from "element-ui"'),
    parseImportDeclaration('import { FormItem } from "element-ui"'),
  ],
  v3: [
    parseImportDeclaration('import { ElForm } from "element-plus"'),
    parseImportDeclaration('import { ElRow } from "element-plus"'),
    parseImportDeclaration('import { ElCol  } from "element-plus"'),
    parseImportDeclaration('import { ElFormItem } from "element-plus"'),
  ],
};

const buildImportsAndRegister: BuildStep<{
  imports: string;
  componentRegisterObjLiteral: string;
}> = ({ items, flags, config }) => {
  const { v2, JSX, shoulImportBuiltInComps } = flags;

  const finalComponents = [];
  const finalComponentImportDeclarations: BaseImportDeclaration[] = [];

  const {
    customComponents,
    customComponentImportDeclarations,
    builtInComponents,
    builtInComponentImportDeclarations,
    rulesImportDeclarations,
  } = refineFormItems(items);

  if (config.importedComponents !== VueComponetImport.None) {
    const extraBuiltInComponentImportDeclarations = cloneDeep(
      v2 ? NECESSARY_IDS.v2 : NECESSARY_IDS.v3
    );
    const extraBuiltInComponents =
      extraBuiltInComponentImportDeclarations.map<ParsedComponent>((d) => ({
        id: v4(),
        name: "built-in",
        custom: false,
        import: cloneDeep(d),
      }));
    builtInComponents.unshift(...extraBuiltInComponents);
    builtInComponentImportDeclarations.unshift(
      ...extraBuiltInComponentImportDeclarations
    );
    switch (config.importedComponents) {
      case VueComponetImport.All:
        finalComponents.push(...builtInComponents, ...customComponents);
        finalComponentImportDeclarations.push(
          ...builtInComponentImportDeclarations,
          ...customComponentImportDeclarations
        );
        break;
      case VueComponetImport.BuiltIn:
        finalComponents.push(...builtInComponents);
        finalComponentImportDeclarations.push(
          ...builtInComponentImportDeclarations
        );
        break;
      case VueComponetImport.Custom:
        finalComponents.push(...customComponents);
        finalComponentImportDeclarations.unshift(
          ...customComponentImportDeclarations
        );
        break;
    }
  }

  let componentRegisterObjLiteral: string;
  /**
   * 导入并注册内置的element-ui组件
   * element-ui组件导出示例：import { Input } from "element-ui"
   * 如果注册的组件对象的写法为：{ Input }（shorthand），模板中不能使用<el-input />，而且会和原生元素input发生冲突。
   * 因此将注册的组件对象的写法改为：{ ElInput: Input }
   */
  if (v2 && !JSX && shoulImportBuiltInComps) {
    const ret: any[] = [];
    finalComponents.forEach((c) => {
      if (c.custom) {
        // [MyInput]
        ret.push([getFirstVariable(c.import)]);
      } else {
        // [ElInput, Input]
        ret.push(
          ...getAllVariables(c.import).map((name) => [`El${name}`, name])
        );
      }
    });
    componentRegisterObjLiteral = generateObjectLiteral(ret);
  } else {
    componentRegisterObjLiteral = generateObjectLiteral(
      finalComponents
        .map((c) =>
          c.custom ? getFirstVariable(c.import) : getAllVariables(c.import)
        )
        .flat() as string[]
    );
  }

  return {
    imports: mergeImportDeclarations([
      ...finalComponentImportDeclarations,
      ...rulesImportDeclarations,
    ])
      .map(generateImportDeclarationCode)
      .join("\n"),

    componentRegisterObjLiteral,
  };
};

const buildForm: BuildStep = ({ items }) => generateFormObjectLiteral(items);

export function createElementBasedCCB(version: 2 | 3) {
  const build: CodeGenContextBuilder<
    ElementCodeGenConfig,
    ElementCodeGenContext
  > = (items, config) => {
    const flags = {} as Flags;
    flags.v2 = version === 2;
    flags.v3 = version === 3;
    flags.JSX = config.viewSyntax === VueViewSyntax.JSX;
    flags.shoulImportBuiltInComps =
      VueComponetImport.All === config.importedComponents ||
      VueComponetImport.BuiltIn === config.importedComponents;

    const options: Parameters<BuildStep>[0] = {
      flags,
      items,
      config,
    };

    return {
      view: buildView(options),
      formObjLiteral: buildForm(options),
      ruleObjLiteral: buildRule(options),
      ...buildImportsAndRegister(options),
    };
  };

  return build;
}
